import AWS from '../../../../aws/sdk-v2.js'
import crypto from 'crypto'
import fs from 'fs'
import _ from 'lodash'
import path from 'path'
import ServerlessError from '../../../../serverless-error.js'
import deepSortObjectByKey from '../../../../utils/deep-sort-object-by-key.js'
import getHashForFilePath from '../lib/get-hash-for-file-path.js'
import resolveLambdaTarget from '../../utils/resolve-lambda-target.js'
import parseS3URI from '../../utils/parse-s3-uri.js'
import utils from '@serverlessinc/sf-core/src/utils.js'

const { log } = utils

const defaultCors = {
  allowedOrigins: ['*'],
  allowedHeaders: [
    'Content-Type',
    'X-Amz-Date',
    'Authorization',
    'X-Api-Key',
    'X-Amz-Security-Token',
    'X-Amzn-Trace-Id',
  ],
  allowedMethods: ['*'],
}
const runtimeManagementMap = new Map([
  ['auto', 'Auto'],
  ['onFunctionUpdate', 'FunctionUpdate'],
  ['manual', 'Manual'],
])

class AwsCompileFunctions {
  constructor(serverless, options) {
    this.serverless = serverless
    this.options = options
    const serviceDir = this.serverless.serviceDir || ''
    this.packagePath =
      this.serverless.service.package.path ||
      path.join(serviceDir || '.', '.serverless')

    this.provider = this.serverless.getProvider('aws')

    this.ensureTargetExecutionPermission = _.memoize(
      this.ensureTargetExecutionPermission,
    )
    if (
      this.serverless.service.provider.name === 'aws' &&
      this.serverless.service.provider.versionFunctions == null
    ) {
      this.serverless.service.provider.versionFunctions = true
    }

    this.hooks = {
      initialize: () => {
        if (
          this.serverless.service.provider.lambdaHashingVersion ===
            '20200924' &&
          !this.options['enforce-hash-update']
        ) {
          this.serverless._logDeprecation(
            'LAMBDA_HASHING_VERSION_PROPERTY',
            'Resolution of lambda version hashes with the "20200924" algorithm is deprecated.' +
              ' It is highly recommend to migrate to new default algorithm. Please see' +
              ' the deprecation documentation for more details about the migration process.',
          )
        }

        if (
          this.serverless.service.provider.lambdaHashingVersion === '20201221'
        ) {
          this.serverless._logDeprecation(
            'LAMBDA_HASHING_VERSION_PROPERTY',
            'Setting "20201221" for "provider.lambdaHashingVersion" is no longer effective as' +
              ' new hashing algorithm is now used by default. You can safely remove this' +
              ' property from your configuration.',
          )
        }
      },
      'package:compileFunctions': async () =>
        this.downloadPackageArtifacts().then(this.compileFunctions.bind(this)),
    }
  }

  compileRole(newFunction, role) {
    const compiledFunction = newFunction
    if (typeof role === 'string') {
      if (role.startsWith('arn:')) {
        // role is a statically defined iam arn
        compiledFunction.Properties.Role = role
      } else if (role === 'IamRoleLambdaExecution') {
        // role is the default role generated by the framework
        compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] }
      } else {
        // role is a Logical Role Name
        compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] }
        compiledFunction.DependsOn = (compiledFunction.DependsOn || []).concat(
          role,
        )
      }
    } else if ('Fn::GetAtt' in role) {
      // role is an "Fn::GetAtt" object
      compiledFunction.Properties.Role = role
      compiledFunction.DependsOn = (compiledFunction.DependsOn || []).concat(
        role['Fn::GetAtt'][0],
      )
    } else {
      // role is an "Fn::ImportValue" or "Fn::Sub" object
      compiledFunction.Properties.Role = role
    }
  }

  async downloadPackageArtifact(functionName) {
    const { region } = this.options
    const S3 = new AWS.S3({ region })

    const functionObject = this.serverless.service.getFunction(functionName)
    if (functionObject.image) return

    const artifactFilePath =
      _.get(functionObject, 'package.artifact') ||
      _.get(this, 'serverless.service.package.artifact')

    const s3Object = parseS3URI(artifactFilePath)
    if (!s3Object) return
    log.info(`Downloading ${s3Object.Key} from bucket ${s3Object.Bucket}`)
    await new Promise((resolve, reject) => {
      const tmpDir = this.serverless.utils.getTmpDirPath()
      const filePath = path.join(tmpDir, path.basename(s3Object.Key))

      const readStream = S3.getObject(s3Object).createReadStream()

      const writeStream = fs.createWriteStream(filePath)
      readStream
        .pipe(writeStream)
        .on('error', reject)
        .on('close', () => {
          if (functionObject.package.artifact) {
            functionObject.package.artifact = filePath
          } else {
            this.serverless.service.package.artifact = filePath
          }
          return resolve(filePath)
        })
    })
  }

  async addFileToHash(filePath, hash) {
    const lambdaHashingVersion =
      this.serverless.service.provider.lambdaHashingVersion
    if (
      lambdaHashingVersion < 20201221 &&
      !this.options['enforce-hash-update']
    ) {
      await addFileContentsToHashes(filePath, [hash])
    } else {
      const filePathHash = await getHashForFilePath(filePath)
      hash.write(filePathHash)
    }
  }

  async compileFunction(functionName) {
    const cfTemplate =
      this.serverless.service.provider.compiledCloudFormationTemplate
    const functionResource = this.cfLambdaFunctionTemplate()
    const functionObject = this.serverless.service.getFunction(functionName)
    functionObject.package = functionObject.package || {}
    const enforceHashUpdate = this.options['enforce-hash-update']

    if (!functionObject.handler && !functionObject.image) {
      throw new ServerlessError(
        `Either "handler" or "image" property needs to be set on function "${functionName}"`,
        'FUNCTION_NEITHER_HANDLER_NOR_IMAGE_DEFINED_ERROR',
      )
    }
    if (functionObject.handler && functionObject.image) {
      throw new ServerlessError(
        `Either "handler" or "image" property (not both) needs to be set on function "${functionName}".`,
        'FUNCTION_BOTH_HANDLER_AND_IMAGE_DEFINED_ERROR',
      )
    }

    let functionImageUri
    let functionImageSha

    if (functionObject.image) {
      ;({ functionImageUri, functionImageSha } =
        await this.provider.resolveImageUriAndSha(functionName))
      if (_.isObject(functionObject.image)) {
        const imageConfig = {}
        if (functionObject.image.command) {
          imageConfig.Command = functionObject.image.command
        }

        if (functionObject.image.entryPoint) {
          imageConfig.EntryPoint = functionObject.image.entryPoint
        }

        if (functionObject.image.workingDirectory) {
          imageConfig.WorkingDirectory = functionObject.image.workingDirectory
        }

        if (Object.keys(imageConfig).length) {
          functionResource.Properties.ImageConfig = imageConfig
        }
      }
    }

    // publish these properties to the platform
    functionObject.memory =
      functionObject.memorySize ||
      this.serverless.service.provider.memorySize ||
      1024
    if (!functionObject.timeout) {
      functionObject.timeout = this.serverless.service.provider.timeout || 6
    }

    let artifactFilePath

    if (functionObject.handler) {
      const serviceArtifactFileName =
        this.provider.naming.getServiceArtifactName()
      const functionArtifactFileName =
        this.provider.naming.getFunctionArtifactName(functionName)

      artifactFilePath =
        functionObject.package.artifact ||
        this.serverless.service.package.artifact

      artifactFilePath =
        artifactFilePath &&
        path.resolve(this.serverless.serviceDir, artifactFilePath)

      if (
        !artifactFilePath ||
        (this.serverless.service.artifact && !functionObject.package.artifact)
      ) {
        let artifactFileName = serviceArtifactFileName
        if (
          this.serverless.service.package.individually ||
          functionObject.package.individually
        ) {
          artifactFileName = functionArtifactFileName
        }

        artifactFilePath = path.join(
          this.serverless.serviceDir,
          '.serverless',
          artifactFileName,
        )
      }

      const runtimeManagement = this.provider.resolveFunctionRuntimeManagement(
        functionObject.runtimeManagement,
      )
      if (runtimeManagement.mode !== 'auto') {
        functionResource.Properties.RuntimeManagementConfig = {
          UpdateRuntimeOn: runtimeManagementMap.get(runtimeManagement.mode),
        }

        if (runtimeManagement.mode === 'manual') {
          functionResource.Properties.RuntimeManagementConfig.RuntimeVersionArn =
            runtimeManagement.arn
        }
      }

      functionObject.runtime = this.provider.getRuntime(functionObject.runtime)
      functionResource.Properties.Handler = functionObject.handler
      functionResource.Properties.Code.S3Bucket = this.serverless.service
        .package.deploymentBucket
        ? this.serverless.service.package.deploymentBucket
        : { Ref: 'ServerlessDeploymentBucket' }

      functionResource.Properties.Code.S3Key = `${
        this.serverless.service.package.artifactDirectoryName
      }/${artifactFilePath.split(path.sep).pop()}`
      functionResource.Properties.Runtime = functionObject.runtime
    } else {
      functionResource.Properties.Code.ImageUri = functionImageUri
      functionResource.Properties.PackageType = 'Image'
    }
    functionResource.Properties.FunctionName = functionObject.name
    functionResource.Properties.MemorySize = functionObject.memory
    functionResource.Properties.Timeout = functionObject.timeout

    const functionArchitecture =
      functionObject.architecture ||
      this.serverless.service.provider.architecture
    if (functionArchitecture)
      functionResource.Properties.Architectures = [functionArchitecture]

    if (functionObject.description) {
      functionResource.Properties.Description = functionObject.description
    }

    if (functionObject.condition) {
      functionResource.Condition = functionObject.condition
    }

    if (functionObject.dependsOn) {
      functionResource.DependsOn = (functionResource.DependsOn || []).concat(
        functionObject.dependsOn,
      )
    }

    if (functionObject.tags || this.serverless.service.provider.tags) {
      const tags = Object.assign(
        {},
        this.serverless.service.provider.tags,
        functionObject.tags,
      )
      functionResource.Properties.Tags = []
      Object.entries(tags).forEach(([Key, Value]) => {
        functionResource.Properties.Tags.push({ Key, Value })
      })
    }

    if (functionObject.ephemeralStorageSize) {
      functionResource.Properties.EphemeralStorage = {
        Size: functionObject.ephemeralStorageSize,
      }
    }

    if (functionObject.onError) {
      const arn = functionObject.onError

      if (typeof arn === 'string') {
        const iamRoleLambdaExecution =
          cfTemplate.Resources.IamRoleLambdaExecution
        functionResource.Properties.DeadLetterConfig = {
          TargetArn: arn,
        }

        // update the PolicyDocument statements (if default policy is used)
        if (iamRoleLambdaExecution) {
          iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push(
            {
              Effect: 'Allow',
              Action: ['sns:Publish'],
              Resource: [arn],
            },
          )
        }
      } else {
        functionResource.Properties.DeadLetterConfig = {
          TargetArn: arn,
        }
      }
    }

    let kmsKeyArn
    if (this.serverless.service.provider.kmsKeyArn) {
      kmsKeyArn = this.serverless.service.provider.kmsKeyArn
    }
    if (functionObject.kmsKeyArn) kmsKeyArn = functionObject.kmsKeyArn

    if (kmsKeyArn) {
      if (typeof kmsKeyArn === 'string') {
        functionResource.Properties.KmsKeyArn = kmsKeyArn

        // update the PolicyDocument statements (if default policy is used)
        const iamRoleLambdaExecution =
          cfTemplate.Resources.IamRoleLambdaExecution
        if (iamRoleLambdaExecution) {
          iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement =
            _.unionWith(
              iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument
                .Statement,
              [
                {
                  Effect: 'Allow',
                  Action: ['kms:Decrypt'],
                  Resource: [kmsKeyArn],
                },
              ],
              _.isEqual,
            )
        }
      } else {
        functionResource.Properties.KmsKeyArn = kmsKeyArn
      }
    }

    const tracing =
      functionObject.tracing ||
      (this.serverless.service.provider.tracing &&
        this.serverless.service.provider.tracing.lambda)

    if (tracing) {
      let mode = tracing

      if (typeof tracing === 'boolean') {
        mode = 'Active'
      }

      const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution

      functionResource.Properties.TracingConfig = {
        Mode: mode,
      }

      const stmt = {
        Effect: 'Allow',
        Action: ['xray:PutTraceSegments', 'xray:PutTelemetryRecords'],
        Resource: ['*'],
      }

      // update the PolicyDocument statements (if default policy is used)
      if (iamRoleLambdaExecution) {
        iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement =
          _.unionWith(
            iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument
              .Statement,
            [stmt],
            _.isEqual,
          )
      }
    }

    if (
      functionObject.environment ||
      this.serverless.service.provider.environment
    ) {
      functionResource.Properties.Environment = {}
      functionResource.Properties.Environment.Variables = Object.assign(
        {},
        this.serverless.service.provider.environment,
        functionObject.environment,
      )
    }

    const role = this.provider.getCustomExecutionRole(functionObject)
    this.compileRole(functionResource, role || 'IamRoleLambdaExecution')

    // ensure provider VPC is not used if function VPC explicitly unset
    if (functionObject.vpc !== null && functionObject.vpc !== false) {
      if (!functionObject.vpc) functionObject.vpc = {}
      if (!this.serverless.service.provider.vpc)
        this.serverless.service.provider.vpc = {}

      functionResource.Properties.VpcConfig = {
        SecurityGroupIds:
          functionObject.vpc.securityGroupIds ||
          this.serverless.service.provider.vpc.securityGroupIds,
        SubnetIds:
          functionObject.vpc.subnetIds ||
          this.serverless.service.provider.vpc.subnetIds,
      }

      if (
        !functionResource.Properties.VpcConfig.SecurityGroupIds ||
        !functionResource.Properties.VpcConfig.SubnetIds
      ) {
        delete functionResource.Properties.VpcConfig
      }
    }

    const fileSystemConfig = functionObject.fileSystemConfig

    if (fileSystemConfig) {
      if (!functionResource.Properties.VpcConfig) {
        const errorMessage = [
          `Function "${functionName}": when using fileSystemConfig, `,
          'ensure that function has vpc configured ',
          'on function or provider level',
        ].join('')
        throw new ServerlessError(
          errorMessage,
          'LAMBDA_FILE_SYSTEM_CONFIG_MISSING_VPC',
        )
      }

      const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution

      const stmt = {
        Effect: 'Allow',
        Action: [
          'elasticfilesystem:ClientMount',
          'elasticfilesystem:ClientWrite',
        ],
        Resource: [fileSystemConfig.arn],
      }

      // update the PolicyDocument statements (if default policy is used)
      if (iamRoleLambdaExecution) {
        iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push(
          stmt,
        )
      }

      const cfFileSystemConfig = {
        Arn: fileSystemConfig.arn,
        LocalMountPath: fileSystemConfig.localMountPath,
      }

      functionResource.Properties.FileSystemConfigs = [cfFileSystemConfig]
    }

    if (
      functionObject.reservedConcurrency ||
      functionObject.reservedConcurrency === 0
    ) {
      functionResource.Properties.ReservedConcurrentExecutions =
        functionObject.reservedConcurrency
    }

    if (
      !functionObject.disableLogs &&
      !functionObject?.logs?.logGroup &&
      !this.serverless.service.provider.logs?.lambda?.logGroup
    ) {
      functionResource.DependsOn = [
        this.provider.naming.getLogGroupLogicalId(functionName),
      ].concat(functionResource.DependsOn || [])
    }

    if (functionObject.layers) {
      functionResource.Properties.Layers = functionObject.layers
    } else if (this.serverless.service.provider.layers) {
      // To avoid unwanted side effects ensure to not reference same array instace on each function
      functionResource.Properties.Layers = Array.from(
        this.serverless.service.provider.layers,
      )
    }

    const functionLogicalId =
      this.provider.naming.getLambdaLogicalId(functionName)
    const newFunctionObject = {
      [functionLogicalId]: functionResource,
    }

    Object.assign(cfTemplate.Resources, newFunctionObject)

    const shouldVersionFunction =
      functionObject.versionFunction != null
        ? functionObject.versionFunction
        : this.serverless.service.provider.versionFunctions

    if (
      shouldVersionFunction ||
      functionObject.provisionedConcurrency ||
      functionObject.snapStart
    ) {
      // Create hashes for the artifact and the logical id of the version resource
      // The one for the version resource must include the function configuration
      // to make sure that a new version is created on configuration changes and
      // not only on source changes.

      if (enforceHashUpdate) {
        functionResource.Properties.Description =
          'temporary-description-to-enforce-hash-update'
      }

      const versionHash = crypto.createHash('sha256')
      versionHash.setEncoding('base64')
      const layerConfigurations = _.cloneDeep(
        extractLayerConfigurationsFromFunction(
          functionResource.Properties,
          cfTemplate,
        ),
      )

      const versionResource = this.cfLambdaVersionTemplate()

      if (functionImageSha) {
        versionResource.Properties.CodeSha256 = functionImageSha
      } else {
        const fileHash = await getHashForFilePath(artifactFilePath)
        versionResource.Properties.CodeSha256 = fileHash

        await this.addFileToHash(artifactFilePath, versionHash)
      }
      // Include all referenced layer code in the version id hash
      const layerArtifactPaths = []
      layerConfigurations.forEach((layer) => {
        const layerArtifactPath = this.provider.resolveLayerArtifactName(
          layer.name,
        )
        layerArtifactPaths.push(layerArtifactPath)
      })

      for (const layerArtifactPath of layerArtifactPaths.sort()) {
        await this.addFileToHash(layerArtifactPath, versionHash)
      }

      // Include function and layer configuration details in the version id hash
      for (const layerConfig of layerConfigurations) {
        delete layerConfig.properties.Content.S3Key
      }

      const functionProperties = _.cloneDeep(functionResource.Properties)
      // In `image` case, we assume it's path to ECR image digest
      if (!functionObject.image) delete functionProperties.Code
      // Properties applied to function globally (not specific to version or alias)
      delete functionProperties.ReservedConcurrentExecutions
      delete functionProperties.Tags

      const lambdaHashingVersion =
        this.serverless.service.provider.lambdaHashingVersion
      if (
        lambdaHashingVersion < 20201221 &&
        !this.options['enforce-hash-update']
      ) {
        // sort the layer configurations for hash consistency
        const sortedLayerConfigurations = {}
        const byKey = ([key1], [key2]) => key1.localeCompare(key2)
        for (const {
          name,
          properties: layerProperties,
        } of layerConfigurations) {
          sortedLayerConfigurations[name] = _.fromPairs(
            Object.entries(layerProperties).sort(byKey),
          )
        }
        functionProperties.layerConfigurations = sortedLayerConfigurations
        const sortedFunctionProperties = _.fromPairs(
          Object.entries(functionProperties).sort(byKey),
        )

        versionHash.write(JSON.stringify(sortedFunctionProperties))
      } else {
        functionProperties.layerConfigurations = layerConfigurations
        versionHash.write(
          JSON.stringify(deepSortObjectByKey(functionProperties)),
        )
      }

      versionHash.end()
      const versionDigest = versionHash.read()

      versionResource.Properties.FunctionName = { Ref: functionLogicalId }
      if (functionObject.description) {
        versionResource.Properties.Description = functionObject.description
      }

      // use the version SHA in the logical resource ID of the version because
      // AWS::Lambda::Version resource will not support updates
      const versionLogicalId = this.provider.naming.getLambdaVersionLogicalId(
        functionName,
        versionDigest,
      )
      functionObject.versionLogicalId = versionLogicalId
      const newVersionObject = {
        [versionLogicalId]: versionResource,
      }

      Object.assign(cfTemplate.Resources, newVersionObject)

      // Add function versions to Outputs section
      const functionVersionOutputLogicalId =
        this.provider.naming.getLambdaVersionOutputLogicalId(functionName)
      const newVersionOutput = this.cfOutputLatestVersionTemplate()

      newVersionOutput.Value = { Ref: versionLogicalId }

      Object.assign(cfTemplate.Outputs, {
        [functionVersionOutputLogicalId]: newVersionOutput,
      })

      if (functionObject.provisionedConcurrency && functionObject.snapStart) {
        throw new ServerlessError(
          `Functions with enabled SnapStart does not support provisioned concurrency. Please remove at least one of the settings on function "${functionName}".`,
          'FUNCTION_BOTH_PROVISIONED_CONCURRENCY_AND_SNAPSTART_ENABLED_ERROR',
        )
      }

      if (functionObject.provisionedConcurrency) {
        if (!shouldVersionFunction) delete versionResource.DeletionPolicy

        const aliasLogicalId =
          this.provider.naming.getLambdaProvisionedConcurrencyAliasLogicalId(
            functionName,
          )
        const aliasName =
          this.provider.naming.getLambdaProvisionedConcurrencyAliasName()

        functionObject.targetAlias = {
          name: aliasName,
          logicalId: aliasLogicalId,
        }

        const aliasResource = {
          Type: 'AWS::Lambda::Alias',
          Properties: {
            FunctionName: { Ref: functionLogicalId },
            FunctionVersion: { 'Fn::GetAtt': [versionLogicalId, 'Version'] },
            Name: aliasName,
            ProvisionedConcurrencyConfig: {
              ProvisionedConcurrentExecutions:
                functionObject.provisionedConcurrency,
            },
          },
          DependsOn: functionLogicalId,
        }

        cfTemplate.Resources[aliasLogicalId] = aliasResource
      }

      if (functionObject.snapStart) {
        if (!shouldVersionFunction) delete versionResource.DeletionPolicy

        functionResource.Properties.SnapStart = {
          ApplyOn: 'PublishedVersions',
        }

        const aliasLogicalId =
          this.provider.naming.getLambdaSnapStartAliasLogicalId(functionName)
        const aliasName =
          this.provider.naming.getLambdaSnapStartEnabledAliasName()

        functionObject.targetAlias = {
          name: aliasName,
          logicalId: aliasLogicalId,
        }

        const aliasResource = {
          Type: 'AWS::Lambda::Alias',
          Properties: {
            FunctionName: { Ref: functionLogicalId },
            FunctionVersion: { 'Fn::GetAtt': [versionLogicalId, 'Version'] },
            Name: aliasName,
          },
          DependsOn: functionLogicalId,
        }

        cfTemplate.Resources[aliasLogicalId] = aliasResource
      }
    }

    if (functionObject.logs || this.serverless.service.provider.logs?.lambda) {
      const functionLogConfig = functionObject.logs
      const providerLogConfig = this.serverless.service?.provider?.logs?.lambda
      const applicationLogLevel =
        functionLogConfig?.applicationLogLevel ||
        providerLogConfig?.applicationLogLevel
      const logFormat =
        functionLogConfig?.logFormat || providerLogConfig?.logFormat
      const logGroup =
        functionLogConfig?.logGroup || providerLogConfig?.logGroup
      const systemLogLevel =
        functionLogConfig?.systemLogLevel || providerLogConfig?.systemLogLevel

      const finalizedLogConfiguration = {}
      if (applicationLogLevel && logFormat && logFormat === 'JSON') {
        finalizedLogConfiguration.ApplicationLogLevel = applicationLogLevel
      }
      if (logFormat) {
        finalizedLogConfiguration.LogFormat = logFormat
      }
      if (logGroup) {
        finalizedLogConfiguration.LogGroup = logGroup
      }
      if (systemLogLevel && logFormat && logFormat === 'JSON') {
        finalizedLogConfiguration.SystemLogLevel = systemLogLevel
      }

      if (Object.keys(finalizedLogConfiguration).length > 0) {
        functionResource.Properties.LoggingConfig = finalizedLogConfiguration
      }
    }

    this.compileFunctionUrl(functionName)
    this.compileFunctionEventInvokeConfig(functionName)
  }

  compileFunctionUrl(functionName) {
    const functionObject = this.serverless.service.getFunction(functionName)
    const cfTemplate =
      this.serverless.service.provider.compiledCloudFormationTemplate
    const { url } = functionObject

    if (!url) return

    let auth = 'NONE'
    let cors = null
    if (url.authorizer === 'aws_iam') {
      auth = 'AWS_IAM'
    }

    if (url.cors) {
      cors = Object.assign({}, defaultCors)

      if (url.cors.allowedOrigins) {
        cors.allowedOrigins = _.uniq(url.cors.allowedOrigins)
      } else if (url.cors.allowedOrigins === null) {
        delete cors.allowedOrigins
      }

      if (url.cors.allowedHeaders) {
        cors.allowedHeaders = _.uniq(url.cors.allowedHeaders)
      } else if (url.cors.allowedHeaders === null) {
        delete cors.allowedHeaders
      }

      if (url.cors.allowedMethods) {
        cors.allowedMethods = _.uniq(url.cors.allowedMethods)
      } else if (url.cors.allowedMethods === null) {
        delete cors.allowedMethods
      }

      if (url.cors.allowCredentials) cors.allowCredentials = true

      if (url.cors.exposedResponseHeaders) {
        cors.exposedResponseHeaders = _.uniq(url.cors.exposedResponseHeaders)
      }

      cors.maxAge = url.cors.maxAge
    }

    const urlResource = {
      Type: 'AWS::Lambda::Url',
      Properties: {
        AuthType: auth,
        TargetFunctionArn: resolveLambdaTarget(functionName, functionObject),
      },
      DependsOn: _.get(functionObject.targetAlias, 'logicalId'),
    }

    if (cors) {
      urlResource.Properties.Cors = {
        AllowCredentials: cors.allowCredentials,
        AllowHeaders: cors.allowedHeaders && Array.from(cors.allowedHeaders),
        AllowMethods: cors.allowedMethods && Array.from(cors.allowedMethods),
        AllowOrigins: cors.allowedOrigins && Array.from(cors.allowedOrigins),
        ExposeHeaders:
          cors.exposedResponseHeaders &&
          Array.from(cors.exposedResponseHeaders),
        MaxAge: cors.maxAge,
      }
    }

    if (url.invokeMode === 'RESPONSE_STREAM') {
      urlResource.Properties.InvokeMode = url.invokeMode
    }

    const logicalId =
      this.provider.naming.getLambdaFunctionUrlLogicalId(functionName)
    cfTemplate.Resources[logicalId] = urlResource
    cfTemplate.Outputs[
      this.provider.naming.getLambdaFunctionUrlOutputLogicalId(functionName)
    ] = {
      Description: 'Lambda Function URL',
      Value: {
        'Fn::GetAtt': [logicalId, 'FunctionUrl'],
      },
    }

    if (auth === 'NONE') {
      cfTemplate.Resources[
        this.provider.naming.getLambdaFnUrlPermissionLogicalId(functionName)
      ] = {
        Type: 'AWS::Lambda::Permission',
        Properties: {
          FunctionName: resolveLambdaTarget(functionName, functionObject),
          Action: 'lambda:InvokeFunctionUrl',
          Principal: '*',
          FunctionUrlAuthType: auth,
        },
        DependsOn: _.get(functionObject.targetAlias, 'logicalId'),
      }
    }
  }

  compileFunctionEventInvokeConfig(functionName) {
    const functionObject = this.serverless.service.getFunction(functionName)
    const { destinations, maximumEventAge, maximumRetryAttempts } =
      functionObject

    if (!destinations && !maximumEventAge && maximumRetryAttempts == null) {
      return
    }

    const destinationConfig = {}

    if (destinations) {
      const executionRole = this.provider.getCustomExecutionRole(functionObject)
      const hasAccessPoliciesHandledExternally = Boolean(executionRole)

      if (destinations.onSuccess) {
        destinationConfig.OnSuccess = {
          Destination: this.getDestinationsArn(destinations.onSuccess),
        }

        if (!hasAccessPoliciesHandledExternally) {
          this.ensureTargetExecutionPermission(destinations.onSuccess)
        }
      }

      if (destinations.onFailure) {
        destinationConfig.OnFailure = {
          Destination: this.getDestinationsArn(destinations.onFailure),
        }

        if (!hasAccessPoliciesHandledExternally) {
          this.ensureTargetExecutionPermission(destinations.onFailure)
        }
      }
    }

    const cfResources =
      this.serverless.service.provider.compiledCloudFormationTemplate.Resources
    const functionLogicalId =
      this.provider.naming.getLambdaLogicalId(functionName)

    const resource = {
      Type: 'AWS::Lambda::EventInvokeConfig',
      Properties: {
        FunctionName: { Ref: functionLogicalId },
        DestinationConfig: destinationConfig,
        Qualifier: functionObject.targetAlias
          ? functionObject.targetAlias.name
          : '$LATEST',
      },
      DependsOn: _.get(functionObject.targetAlias, 'logicalId'),
    }

    if (maximumEventAge) {
      resource.Properties.MaximumEventAgeInSeconds = maximumEventAge
    }

    if (maximumRetryAttempts != null) {
      resource.Properties.MaximumRetryAttempts = maximumRetryAttempts
    }

    cfResources[
      this.provider.naming.getLambdaEventConfigLogicalId(functionName)
    ] = resource
  }

  getDestinationsArn(destinationsProperty) {
    if (typeof destinationsProperty === 'object') {
      return destinationsProperty.arn
    }
    return destinationsProperty.startsWith('arn:')
      ? destinationsProperty
      : this.provider.resolveFunctionArn(destinationsProperty)
  }

  // Memoized in a constructor
  ensureTargetExecutionPermission(destinationsProperty) {
    const iamPolicyStatements =
      this.serverless.service.provider.compiledCloudFormationTemplate.Resources
        .IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement

    const action = (() => {
      if (typeof destinationsProperty === 'object') {
        if (destinationsProperty.type === 'function')
          return 'lambda:InvokeFunction'
        if (destinationsProperty.type === 'sqs') return 'sqs:SendMessage'
        if (destinationsProperty.type === 'sns') return 'sns:Publish'
        if (destinationsProperty.type === 'eventBus') return 'events:PutEvents'
      }

      if (typeof destinationsProperty === 'string') {
        if (
          !destinationsProperty.startsWith('arn:') ||
          destinationsProperty.includes(':function:')
        ) {
          return 'lambda:InvokeFunction'
        }
        if (destinationsProperty.includes(':sqs:')) return 'sqs:SendMessage'
        if (destinationsProperty.includes(':sns:')) return 'sns:Publish'
        if (destinationsProperty.includes(':event-bus/'))
          return 'events:PutEvents'
      }

      throw new ServerlessError(
        `Unsupported destination target ${destinationsProperty}`,
        'UNSUPPORTED_DESTINATION_TARGET',
      )
    })()

    let ResourceArn
    if (typeof destinationsProperty === 'object') {
      ResourceArn = destinationsProperty.arn
    } else {
      // Note: Cannot address function via { 'Fn::GetAtt': [targetLogicalId, 'Arn'] }
      // as same IAM settings are used for target function and that will introduce
      // circular dependency error. Relying on Fn::Sub as a workaround
      ResourceArn = destinationsProperty.startsWith('arn:')
        ? destinationsProperty
        : {
            'Fn::Sub': `arn:\${AWS::Partition}:lambda:\${AWS::Region}:\${AWS::AccountId}:function:${
              this.serverless.service.getFunction(destinationsProperty).name
            }`,
          }
    }
    iamPolicyStatements.push({
      Effect: 'Allow',
      Action: action,
      Resource: ResourceArn,
    })
  }

  async downloadPackageArtifacts() {
    const allFunctions = this.serverless.service.getAllFunctions()
    // download package artifact sequentially one after another
    for (const functionName of allFunctions) {
      await this.downloadPackageArtifact(functionName)
    }
  }

  async compileFunctions() {
    const allFunctions = this.serverless.service.getAllFunctions()
    return Promise.all(
      allFunctions.map((functionName) => this.compileFunction(functionName)),
    )
  }

  cfLambdaFunctionTemplate() {
    return {
      Type: 'AWS::Lambda::Function',
      Properties: {
        Code: {},
      },
    }
  }

  cfLambdaVersionTemplate() {
    return {
      Type: 'AWS::Lambda::Version',
      // Retain old versions even though they will not be in future
      // CloudFormation stacks. On stack delete, these will be removed when
      // their associated function is removed.
      DeletionPolicy: 'Retain',
      Properties: {
        FunctionName: 'FunctionName',
        CodeSha256: 'CodeSha256',
      },
    }
  }

  cfOutputLatestVersionTemplate() {
    return {
      Description: 'Current Lambda function version',
      Value: 'Value',
    }
  }
}

async function addFileContentsToHashes(filePath, hashes) {
  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(filePath)
    readStream
      .on('data', (chunk) => {
        hashes.forEach((hash) => {
          hash.write(chunk)
        })
      })
      .on('close', () => {
        resolve()
      })
      .on('error', (error) => {
        reject(new Error(`Could not add file content to hash: ${error}`))
      })
  })
}

function extractLayerConfigurationsFromFunction(
  functionProperties,
  cfTemplate,
) {
  const layerConfigurations = []
  if (!functionProperties.Layers) return layerConfigurations
  functionProperties.Layers.forEach((potentialLocalLayerObject) => {
    if (potentialLocalLayerObject.Ref) {
      const configuration = cfTemplate.Resources[potentialLocalLayerObject.Ref]

      if (!configuration) {
        log.info(
          `Could not find reference to layer: ${potentialLocalLayerObject.Ref}.`,
        )
        return
      }

      layerConfigurations.push({
        name: configuration._serverlessLayerName,
        ref: potentialLocalLayerObject.Ref,
        properties: configuration.Properties,
      })
    }
  })
  return layerConfigurations
}

export default AwsCompileFunctions
